Knapsack problem
The knapsack problem is a family of combinatorial optimisation problems: given a set of items each with a weight and a value, and a knapsack of fixed capacity, choose items to maximise total value without exceeding capacity. Despite being NP-complete in general — no algorithm can solve every instance in time polynomial in the description length — all standard variants admit dynamic programming solutions running in time, where is the number of items and is the capacity. Because appears linearly rather than logarithmically, this is only pseudo-polynomial, but it is fast enough for the , constraints typical of contests. Knapsack is one of the most frequently recurring DP patterns in competitive programming, and understanding its variants — bounded, unbounded, group, tree, fractional, and meet-in-the-middle — covers a large fraction of the "selection under budget" problems that appear on every major judge.
Description
0/1 knapsack
The standard formulation: items, item with integer weight and integer value , knapsack capacity . Each item may be taken at most once. Maximise total value subject to total weight
Define = maximum total value obtainable using a subset of the first items with total weight . The recurrence considers two cases for item : skip it (inheriting ) or take it (adding and consuming capacity):
where the second option requires . The 2-D table has entries and can be filled row by row in time and space. Space reduces to by rolling the -dimension: maintain only one 1-D array, and process capacity right to left so that still holds the -th row value when it is read. Scanning left to right instead would allow item to be used multiple times — which is exactly the unbounded variant below.
// 0/1 knapsack: n items with weights w[], values v[], capacity W
// dp[c] = max value achievable with total weight <= c
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++)
for (int c = W; c >= w[i]; c--) // right-to-left: each item at most once
dp[c] = max(dp[c], dp[c - w[i]] + v[i]);
// answer: dp[W]
Complexity
Time , space with the rolling optimisation. The complexity is pseudo-polynomial: is encoded in bits, so is exponential in the input length — fully consistent with knapsack being NP-complete. In practice, contest problems keep or so, and the constant factor of the inner loop is small.
Applications
- Subset-sum — deciding whether some subset has total weight exactly is 0/1 knapsack with all values equal to 1 (or a boolean DP array). This is the canonical NP-complete decision problem, appearing as a special case of knapsack and as a subroutine in countless contest problems.
- Coin change — producing a target amount from denominations (each usable many times) is unbounded knapsack. Minimising the number of coins swaps max for min; counting the number of distinct multisets uses the counting formulation. These are among the most common DP problems in contests.
- Budget-constrained scheduling — selecting jobs, projects, or items under a time or cost budget to maximise profit maps directly onto 0/1 or bounded knapsack, depending on whether each option may be chosen once or a limited number of times.
- LP relaxation — the fractional knapsack variant is the linear programming relaxation of the 0/1 problem. Its greedy optimum provides a tight upper bound on the integer solution, and branch-and-bound solvers use this bound at every node of their search tree.
Variants
Unbounded knapsack
Each item may be used any number of times. The only change from 0/1 is to scan capacity left to right, so that when is read it already reflects the possibility of having used item earlier in this same pass:
// Unbounded knapsack: each item usable unlimited times
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++)
for (int c = w[i]; c <= W; c++) // left-to-right: reuse allowed
dp[c] = max(dp[c], dp[c - w[i]] + v[i]);
Coin change — minimum coins to reach amount — is the same recurrence with min
and an initialisation of (use INT_MAX / 2 to avoid overflow):
// Minimum coins: dp[c] = fewest coins to make amount c
vector<int> dp(W + 1, INT_MAX / 2);
dp[0] = 0;
for (int c = 1; c <= W; c++)
for (int i = 0; i < n; i++)
if (c >= w[i])
dp[c] = min(dp[c], dp[c - w[i]] + 1);
Counting subsets and the bitset trick
Replace max with addition to count the number of subsets achieving each total weight. For pure feasibility ("is weight achievable?") the DP is boolean and each update is a logical OR. When weights are integers, the bitwise representation of the reachability vector lets an entire pass over one item's weight be done with a single left-shift and OR, reducing the per-item inner loop from to
// Subset-sum feasibility with bitset: O(n * T / 64)
// reachable[c] = 1 iff some subset of the first i items has total weight exactly c
bitset<MAXW> reachable;
reachable[0] = 1;
for (int i = 0; i < n; i++)
reachable |= reachable << w[i];
// reachable[T] = 1 iff target T is achievable
The intuition: reachable << w[i] shifts every achievable weight up by ,
producing the set of weights reachable by adding one copy of item to any
previously reachable subset. OR-ing with the original covers both "take" and "skip".
Fractional knapsack
Items may be split: taking fraction of item contributes weight and value . This variant is solved greedily in : sort items by value density descending, fill the knapsack greedily in that order, and split the last item to fill remaining capacity exactly. The greedy works because taking more of the highest-density item is always at least as good as taking any of a lower-density item. The fractional solution gives the LP relaxation of the integer 0/1 problem and is always an upper bound on the 0/1 optimum.
Bounded knapsack
Item is available times. The naïve reduction to 0/1 knapsack — replicate each item times — costs , which is too slow when counts are large.
Binary grouping brings this down to where 12. The idea is to represent the copies of item using a small number of virtual 0/1 items: take groups of sizes (powers of two) up to a remainder that makes the total exactly . Because every integer in can be written as a sum of a subset of these group sizes (this follows from the standard binary representation argument), the virtual items can collectively reproduce any valid count from to . Running the standard 0/1 knapsack on the virtual items per original item is therefore exact.
// Bounded knapsack via binary grouping: O(n W log C)
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
int rem = cnt[i];
for (int k = 1; rem > 0; k *= 2) {
int take = min(k, rem);
rem -= take;
int wt = take * w[i], vl = take * v[i];
for (int c = W; c >= wt; c--)
dp[c] = max(dp[c], dp[c - wt] + vl);
}
}
Monotone deque achieves a strictly solution — no log factor — by recognising that, for a fixed item type , the capacities can be partitioned into independent residue classes modulo , and within each class the DP update reduces to a sliding-window maximum
Fix residue and let denote the old DP value for the -th capacity in this class. The update for bounded knapsack says we can take copies of item , so the new value is
Substituting (the slot we "pull from") and rearranging:
The quantity depends only on the old value , so it can be precomputed for each . The maximum of over the window of size is a standard sliding-window maximum problem, solved in amortised per step with a monotone deque:
// Bounded knapsack with monotone deque: O(n W)
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
vector<int> old = dp; // snapshot of dp before processing item i
for (int r = 0; r < w[i] && r <= W; r++) {
// process the residue class r: capacities r, r+w[i], r+2*w[i], ...
deque<pair<int,int>> dq; // (index j, f(j) = old[r + j*w[i]] - j*v[i])
int maxj = (W - r) / w[i];
for (int j = 0; j <= maxj; j++) {
int idx = r + j * w[i];
int fj = old[idx] - j * v[i];
// remove indices that have left the window [j - cnt[i], j]
while (!dq.empty() && dq.front().first < j - cnt[i])
dq.pop_front();
// maintain decreasing f-values; dominated entries can never be max
while (!dq.empty() && dq.back().second <= fj)
dq.pop_back();
dq.push_back({j, fj});
// b_j = (max f in window) + j * v[i]
dp[idx] = dq.front().second + j * v[i];
}
}
}
The old snapshot is necessary because must use the DP values from before
item was processed. Each residue class of elements is handled in time; summing over all residue classes gives per item, and
total.
2D (multi-dimensional) knapsack
Items have two weight dimensions (e.g. weight and volume) with separate capacities and . The DP table extends to and both dimensions are scanned right to left, again ensuring each item is counted at most once:
// 2D knapsack: items with weights w[], volumes vol[], values v[]
vector<vector<int>> dp(W + 1, vector<int>(V + 1, 0));
for (int i = 0; i < n; i++)
for (int c1 = W; c1 >= w[i]; c1--)
for (int c2 = V; c2 >= vol[i]; c2--)
dp[c1][c2] = max(dp[c1][c2], dp[c1 - w[i]][c2 - vol[i]] + v[i]);
The table is in size and the total work is , so both dimensions must be small. Adding a third dimension is straightforward but quickly becomes memory-prohibitive.
Group knapsack
Items are partitioned into groups ; at most one item may be
selected from each group. The key insight is that this is structurally identical to
0/1 knapsack at the group level: process groups one at a time (outer loop), scan
capacity right to left (middle loop) so the old pre-group DP values are always used,
and try every item in the group (inner loop). Because the middle loop reads
dp[c - wt] from the old state, no two items from the same group can both
contribute to a single DP cell:
// Group knapsack: at most one item selected per group
// groups[g] = list of (weight, value) pairs in group g
vector<int> dp(W + 1, 0);
for (auto& grp : groups) {
for (int c = W; c >= 0; c--)
for (auto [wt, vl] : grp)
if (c >= wt)
dp[c] = max(dp[c], dp[c - wt] + vl);
}
This models "buy at most one product from each shop" and configurations where choices within a component are mutually exclusive.
Tree (dependency) knapsack
If selecting item requires that its parent in a rooted tree is also selected, the problem becomes tree knapsack (or dependent knapsack). The key structure is that valid selections form connected subtrees containing the root of each selected component. The standard approach is a post-order DFS, where = max value in 's subtree using capacity (with for , since itself cannot be afforded). Children are merged in one at a time using a knapsack-of-knapsacks merge; the inner loop limit of ensures that the units needed for itself are always reserved:
// Tree knapsack: dp[v][c] = max value in subtree(v) using capacity c,
// with the constraint that any descendant requires v.
int W;
vector<int> adj[MAXN];
int w[MAXN], val[MAXN];
vector<int> dp[MAXN];
void dfs(int v, int par) {
dp[v].assign(W + 1, 0);
for (int c = w[v]; c <= W; c++) dp[v][c] = val[v]; // only v selected
for (int u : adj[v]) {
if (u == par) continue;
dfs(u, v);
// merge subtree u into v's DP; right-to-left on c keeps v's cost reserved
for (int c = W; c >= w[v]; c--)
for (int k = 1; k <= c - w[v]; k++)
dp[v][c] = max(dp[v][c], dp[v][c - k] + dp[u][k]);
}
}
// answer: dp[root][W]
The naive complexity is , but bounding each subtree's DP array to entries reduces the total work to : every pair of nodes meets at the merge of their lowest common ancestor exactly once, and the total number of cells merged over the whole tree is bounded by
An elegant alternative flattens the tree with a DFS-order Euler tour and runs a 1-D DP where "take item " advances one step into 's subtree and "skip " jumps past the entire subtree — see the external links.
Meet in the middle
When but is too large for DP (e.g. weights up to , or real-valued), meet in the middle cuts the exponent in half. The idea: split items into two halves and of each, enumerate all subsets of each half as (weight, value) pairs in time, then match the two halves. After sorting by weight and computing a prefix maximum on values, each -subset can be matched to the best complement in with a single binary search:
using ll = long long;
using pll = pair<ll, ll>;
void enumerate(vector<pll>& items, vector<pll>& out) {
int m = items.size();
for (int mask = 0; mask < (1 << m); mask++) {
ll w = 0, v = 0;
for (int i = 0; i < m; i++)
if (mask >> i & 1) { w += items[i].first; v += items[i].second; }
out.push_back({w, v});
}
}
ll knapsack_mitm(vector<pll> items, ll W) {
int n = items.size(), half = n / 2;
vector<pll> A, B;
vector<pll> left(items.begin(), items.begin() + half);
vector<pll> right(items.begin() + half, items.end());
enumerate(left, A);
enumerate(right, B);
// Sort A by weight; prefix-max on value so A[i].second = best value
// among all A-subsets with weight <= A[i].first
sort(A.begin(), A.end());
for (size_t i = 1; i < A.size(); i++)
A[i].second = max(A[i].second, A[i - 1].second);
ll ans = 0;
for (auto [wb, vb] : B) {
if (wb > W) continue;
// find the best A-subset with weight <= W - wb
auto it = upper_bound(A.begin(), A.end(), pll{W - wb, LLONG_MAX});
if (it != A.begin())
ans = max(ans, vb + prev(it)->second);
}
return ans;
}
Time , space . For this is roughly operations, comfortably within contest limits.
Problems
0/1 knapsack
- Knapsack (Kattis) — textbook formulation; output the max value and the selected item indices.
- Book Shop (CSES) — maximise pages bought under a price budget; standard 0/1 knapsack.
- Walrus Weights (Kattis) — split weights into two piles minimising the difference; run the boolean 0/1 knapsack up to and scan for the nearest achievable weight.
Solution sketch — Walrus Weights
Let be the total weight of all items. We want a subset whose total is as close to as possible. Run the 0/1 boolean knapsack to find all achievable sums up to , then scan from downward to find the largest achievable . The minimum absolute difference between pile totals is
- Roller Coaster Fun (Kattis)
Unbounded knapsack and coin change
- Coin Combinations I (CSES) — count ordered ways to reach target sum using coin denominations (each reusable). Swap the loop order: outer over amounts, inner over coins — each amount is reachable from any previously seen coin choice.
- Coin Combinations II (CSES) — count unordered ways; the standard unbounded knapsack counting formulation (outer over coins, inner over amounts left-to-right).
Solution sketch — Coin Combinations II
Counting unordered multisets requires avoiding permutation duplicates. The fix: the outer loop iterates coin types, the inner loop iterates amounts left-to-right, updating . Each dp value accumulates combinations whose largest coin index is the current one, ensuring each multiset is counted exactly once. The left-to-right scan allows the same coin to appear multiple times in a single pass (unbounded), while the outer coin loop fixes which coin types are "allowed so far".
- Buffed Buffet (Kattis) — buffet items are either discrete (bounded) or continuous (fractional); requires combining both knapsack types.
Subset sum and counting
- Money Sums (CSES) — find all achievable coin subset sums; run the boolean 0/1 knapsack, then collect and count the true entries.
- Two Sets II (CSES) — count ways to partition into two equal-sum subsets; counting 0/1 knapsack on target (answer is 0 if is odd).
Bounded knapsack
- Book Shop II (CSES) — same as Book Shop but each book has a limited print run ; binary grouping or monotone deque required.
- Lucky Country (CF 95E) — bounded knapsack after grouping items by value label; the number of distinct item types is small, making binary grouping efficient 3
Solution sketch — Lucky Country
Group items by their label value: each distinct value appears times, giving a bounded knapsack instance. Since the number of distinct values is at most (if more distinct values existed, their total copies would exceed the capacity), apply binary grouping on each group and run the resulting 0/1 knapsack in time, where is the number of distinct values.
Group knapsack
- Tug of War (BOI 2015 Day 2) — partition players into two teams of almost-equal size and total weight; feasibility for each candidate split is checked with a 2D DP tracking both count and weight, naturally a group-style knapsack.
Tree (dependency) knapsack
- Karen and Supermarket (CF 815C) — goods with a tree of coupon dependencies; select at most goods at minimum cost, choosing whether to apply each coupon (using a coupon requires the parent coupon to also be used).
Solution sketch — Karen and Supermarket
Root the coupon tree at node 1. Define = minimum cost to purchase exactly goods from 's subtree, where means coupon is not used and means it is (discounting 's own price by ). At a leaf : , , . Merging child into is a standard convolution, giving total work by the tree-merge argument. The key constraint: the state for allows each child to independently choose , while for forces for all of 's descendants (no coupon chain without the root coupon).
Meet in the middle
- Subset Sum (CSES) — integers, find a subset summing to target (up to ); the total can reach , far too large for any array-indexed DP, so MITM is the intended approach.
Solution sketch — Subset Sum (CSES 1641)
Split the integers into two halves , of each. Enumerate all subset sums of each half, yielding two lists of (weight, value) pairs. Sort one list, compute its prefix maximum on values, and for each sum in the -list binary-search for the largest in the -list. Total time , space
See also
- Dynamic programming — the general technique underlying knapsack DP
- Meet-in-the-middle — exponential halving for large- or large- variants
- Divide and conquer optimization — another DP speedup for certain knapsack-shaped recurrences with concave or convex structure
- NP-complete — knapsack is the canonical pseudo-polynomial NP-complete problem
- Frobenius coin problem — closely related: which sums are achievable at all with given coin denominations?
- Linear programming — the fractional knapsack is the LP relaxation of 0/1

